/**
 * Copyright 2008 Jeremy Ford
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. \
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/
package name.fordj;

import ca.odell.glazedlists.TransformedList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.event.ListEvent;

import java.util.*;
import static java.util.Locale.ENGLISH;
import java.beans.PropertyDescriptor;
import java.beans.IntrospectionException;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * User: Jeremy Ford
 * Date: Feb 20, 2008
 * Time: 9:46:05 PM
 */
public class IndexedList<T> extends TransformedList<T, T> {
    private static final Class<?> propertyChangeParameters = PropertyChangeListener.class;
    
    private List<T> proxy = new ArrayList<T>();
    private Map<String, Map<Object, List<T>>> indexNameToIndexMap = new HashMap<String, Map<Object,List<T>>>();
    private Map<String, Method> readMethodCache = new HashMap<String, Method>();
    private Method addPropertyChangeListenerMethod;
    private Method removePropertyChangeListenerMethod;
    private Class<T> beanClass;
    private final PropertyChangeListener propertyChangeLister = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent evt) {
            try {
                source.getReadWriteLock().writeLock().lock();

                String property = evt.getPropertyName();
                Map<Object, List<T>> index = indexNameToIndexMap.get(property);
                if(index != null) {
                    T item = beanClass.cast(evt.getSource());
                    removeObjectFromIndex(item, index, evt.getOldValue());

                    //object may have been removed from list due to index change,
                    //check to see if it is still in the list
                    if(contains(item)) {
                        addObjectToIndex(item, index, evt.getNewValue());
                    }
                }
            } finally {
                source.getReadWriteLock().writeLock().unlock();
            }
        }
    };


    /**
     * Creates a {@link ca.odell.glazedlists.TransformedList} to transform the specified {@link ca.odell.glazedlists.EventList}.
     *
     * @param beanClass the class of the object to be stored in the list
     * @param source the {@link ca.odell.glazedlists.EventList} to transform
     */
    public IndexedList(Class<T> beanClass, EventList<T> source) {
        super(source);
        this.beanClass= beanClass;
        initPropertyChangeMethods();
        source.addListEventListener(this);

        proxy.addAll(source);
        for (T t : source) {
            addPropertyChangeListener(t);
        }
    }

    private void addPropertyChangeListener(T t) {
        try {
            addPropertyChangeListenerMethod.invoke(t, propertyChangeLister);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    private void removePropertyChangeListener(T t) {
        try {
            removePropertyChangeListenerMethod.invoke(t, propertyChangeLister);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    private void initPropertyChangeMethods() {
        try {
            addPropertyChangeListenerMethod = beanClass.getMethod("addPropertyChangeListener", propertyChangeParameters);
            removePropertyChangeListenerMethod = beanClass.getMethod("removePropertyChangeListener", propertyChangeParameters);
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("Class " + beanClass + " does not have add/removePropertyChangeListener methods");
        }
    }

    @Override
    protected boolean isWritable() {
        return true;
    }

    public void listChanged(ListEvent<T> listChanges) {
        while(listChanges.next()) {
            final int type = listChanges.getType();
            final int index = listChanges.getIndex();

            if(type == ListEvent.INSERT) {
                T item = source.get(index);
                proxy.add(index, item);
                addObjectToIndexes(item);
                addPropertyChangeListener(item);
            } else if(type == ListEvent.DELETE) {
                T obj = proxy.remove(index);
                removePropertyChangeListener(obj);
                removeObjectFromIndexes(obj);
            } else if(type == ListEvent.UPDATE) {
                T proxyObj = proxy.get(index);
                T obj = source.get(index);

                if(proxyObj != obj) {
                    removePropertyChangeListener(proxyObj);
                    removeObjectFromIndexes(proxyObj);
                    addObjectToIndexes(obj);
                    addPropertyChangeListener(obj);
                    proxy.set(index, obj);
                }
            }
        }
        listChanges.reset();
        updates.forwardEvent(listChanges);
    }

    private void addObjectToIndexes(T item) {
        for (String key : indexNameToIndexMap.keySet()) {
            Map<Object, List<T>> index = indexNameToIndexMap.get(key);
//            if(index == null) {
//                index = new HashMap<Object, List<T>>();
//                indexNameToIndexMap.put(key, index);
//            }
            if(index == null) {
                System.out.println("TEST");
            }
            addObjectToIndex(item, index, getValue(key, item));
        }
    }

    private void addObjectToIndex(T item, Map<Object, List<T>> index, Object value) {
        if(value != null) {
            List<T> items = index.get(value);
            if(items == null) {
                items = new LinkedList<T>();
                index.put(value, items);
            }
            items.add(item);
        }
    }

    private Object getValue(String key, T item) {
        Method readMethod = getReadMethod(key);
        try {
            return readMethod.invoke(item);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    private void removeObjectFromIndexes(T obj) {
        for (String key : indexNameToIndexMap.keySet()) {
            Map<Object, List<T>> index = indexNameToIndexMap.get(key);
            Object value = getValue(key, obj);

            if(!removeObjectFromIndex(obj, index, value)) {
                //if object was not removed with current value,
                //then the indexed value changed, resulting in the deletion event
                //must iterate through all index values, looking for the object
                Iterator<Object> keyIter = index.keySet().iterator();
                while(keyIter.hasNext()) {
                    List<T> items = index.get(keyIter.next());
                    if(items.remove(obj)) {
                        if(items.isEmpty()) {
                            keyIter.remove();
                        }
                        //item cannot be indexed into more than one value, it's ok to break early
                        break;
                    }
                }
            }
        }
    }

    private boolean removeObjectFromIndex(T obj, Map<Object, List<T>> index, Object value) {
        boolean result = false;
        List<T> items = index.get(value);
        if(items != null) {
            result = items.remove(obj);

            if(items.isEmpty()) {
                index.remove(value);
            }
        }
        return result;
    }

//    private void removeObjectFromIndexes(T obj) {
//        for (String key : indexNameToIndexMap.keySet()) {
//            Map<Object, List<T>> index = indexNameToIndexMap.get(key);
//            Set<Object> indexesToRemove = new HashSet<Object>();
//            for (Object indexKey : index.keySet()) {     //this could be brutal depending on the number of objects indexed
//                List<T> items = index.get(indexKey);
//                if(items != null) {
//                    items.remove(obj);
//                }
//                if(items.isEmpty()) {
//                    indexesToRemove.add(indexKey);
//                }
//            }
//            index.keySet().removeAll(indexesToRemove);
//        }
//    }

    public void addIndex(String property) {
        try {
            source.getReadWriteLock().writeLock().lock();
            if(indexNameToIndexMap.containsKey(property)) {
                throw new IllegalStateException("The index " + property + " already exists.");
            }

            Map<Object, List<T>> index = new HashMap<Object,List<T>>();
            indexNameToIndexMap.put(property, index);

            for (T t : source) {
                addObjectToIndex(t, index, getValue(property, t));
                addPropertyChangeListener(t);
            }
        } finally {
            source.getReadWriteLock().writeLock().unlock();
        }
    }

    /**
     * This method assumes that a writeLock is already in place.
     *
     * @param property The property name
     * @return Method
     */
    private Method getReadMethod(String property) {
        Method readMethod = readMethodCache.get(property);
        if(readMethod == null) {
            try {
                String readMethodName = "get" + property.substring(0, 1).toUpperCase(ENGLISH) + property.substring(1);
                PropertyDescriptor pd = new PropertyDescriptor(property, beanClass, readMethodName, null);
                readMethod = pd.getReadMethod();
                readMethodCache.put(property, readMethod);
            } catch (IntrospectionException e) {
                throw new IllegalArgumentException(e);
            }
        }
        return readMethod;
    }

    public void removeIndex(String property) {
        source.getReadWriteLock().writeLock().lock();
        try {
            indexNameToIndexMap.remove(property);
            readMethodCache.remove(property);
        } finally {
            source.getReadWriteLock().writeLock().unlock();
        }
    }

    public boolean hasIndex(String property) {
        source.getReadWriteLock().writeLock().lock();
        try {
            return indexNameToIndexMap.containsKey(property);
        } finally {
            source.getReadWriteLock().writeLock().unlock();
        }
    }

    /**
     * @param property The property name
     * @return Map<Object, List<T>>
     */
    public Map<Object, List<T>> getIndex(String property) {
        try {
            source.getReadWriteLock().readLock().lock();
            return Collections.unmodifiableMap(indexNameToIndexMap.get(property));
        } finally {
            source.getReadWriteLock().readLock().unlock();
        }
    }

    public T getValueByIndex(String property, Object value) {
        try {
            source.getReadWriteLock().readLock().lock();
            Map<Object, List<T>> index = indexNameToIndexMap.get(property);
            if(index == null) {
                throw new IllegalStateException("Index " + property + " does not exist.");
            }
            List<T> values = index.get(value);
            if(values == null || values.isEmpty()) {
                return null;
            }
            return values.get(0);
        } finally {
            source.getReadWriteLock().readLock().unlock();
        }
    }

    public List<T> getValuesByIndex(String property, Object value) {
        try {
            source.getReadWriteLock().readLock().lock();
            Map<Object, List<T>> index = indexNameToIndexMap.get(property);
            if(index == null) {
                throw new IllegalStateException("Index " + property + " does not exist.");
            }
            if(index.get(value) == null) {
                return null;
            }
            return Collections.unmodifiableList(index.get(value));
        } finally {
            source.getReadWriteLock().readLock().unlock();
        }
    }

    public void dispose() {
        super.dispose();
        indexNameToIndexMap.clear();
        readMethodCache.clear();

        try {
            source.getReadWriteLock().writeLock().lock();
            for (T t : source) {
                removePropertyChangeListener(t);
            }
        } finally {
            source.getReadWriteLock().writeLock().unlock();
        }
    }
}